chore: switch to module ESNext + moduleResolution bundler#2095
chore: switch to module ESNext + moduleResolution bundler#2095mattzcarey wants to merge 3 commits into
Conversation
Internal-only build configuration change. Consumers are not affected:
they continue to import from the built `.mjs`/`.d.mts` files declared
in each package's `exports` map.
What changed:
- `common/tsconfig/tsconfig.json`: `module: NodeNext` → `module: ESNext`,
`moduleResolution: NodeNext` → `moduleResolution: bundler`.
- `examples/{client,server}-quickstart/tsconfig.json`: same flip
(they extend a different base and overrode the resolution).
- Strip `.js` extensions from every relative TypeScript import across
packages/, examples/, scripts/, test/.
- Update CLAUDE.md to reflect the new import convention.
Why:
- Removes the long-standing footgun of having to write `from './foo.js'`
in `.ts` source files. Bundler resolution treats the path as a module
reference and lets the tooling resolve it.
- Aligns with what the bundler (`tsdown`), vitest, and downstream
consumers' bundlers actually do at runtime.
Verification: `pnpm typecheck:all`, `pnpm lint:all`, `pnpm test:all`,
`pnpm build:all` all pass.
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
- Make examples/{client,server}-quickstart extend @modelcontextprotocol/tsconfig
instead of carrying standalone compilerOptions. Drop the inline target/lib/
module/moduleResolution/strict/etc. duplicates; only outDir, rootDir,
declaration overrides, and workspace paths remain.
- Align docs/{client,server}-quickstart.md tsconfig snippets to ESNext/bundler
to match what the example projects now compile under.
- Adjust examples/client-quickstart/src/index.ts for the stricter
noUncheckedIndexedAccess inherited from the shared base.
- Strip leftover .js suffixes from vi.mock / vi.importActual string specifiers
in packages/client/test/client/middleware.test.ts.
The shared @modelcontextprotocol/tsconfig sets types: [node, vitest/globals], which the quickstarts inherit even though neither declares vitest as a devDependency — tsc only finds vitest types via the hoisted workspace root node_modules. Override types: [node] on each quickstart compilerOptions to break the accidental coupling.
| "target": "ES2023", | ||
| "lib": ["ES2023"], |
There was a problem hiding this comment.
🟡 After consolidating the quickstart tsconfigs onto @modelcontextprotocol/tsconfig, the examples now inherit target: esnext / lib: [esnext] from the shared base, but docs/client-quickstart.md still shows target: ES2023 / lib: [ES2023] and docs/server-quickstart.md still shows target: ES2022 (no lib). This is the same kind of doc/tsconfig drift the PR already fixed for module/moduleResolution — either bump the docs to esnext or pin target/lib overrides in the example tsconfigs to keep them aligned with what the docs advertise.
Extended reasoning...
What the bug is
Before this PR, examples/client-quickstart/tsconfig.json and examples/server-quickstart/tsconfig.json were standalone configs that matched the tsconfig.json blocks in their respective quickstart docs field-for-field on target/lib (client: ES2023/[ES2023]; server: ES2022/no lib). After this PR, both example tsconfigs were rewritten to "extends": "@modelcontextprotocol/tsconfig" and dropped their own target/lib entries. The shared base at common/tsconfig/tsconfig.json:3-4 sets "target": "esnext" and "lib": ["esnext"], and the example overrides only touch outDir, rootDir, declaration, declarationMap, types, and paths — not target/lib. So both examples now compile with an effective target of esnext, while the docs still tell users to use ES2023 / ES2022.
The specific code path
docs/client-quickstart.md:91-92→"target": "ES2023","lib": ["ES2023"]examples/client-quickstart/tsconfig.json→ extends@modelcontextprotocol/tsconfig, notarget/liboverride → effectiveesnext/[esnext]docs/server-quickstart.md:106→"target": "ES2022", nolibexamples/server-quickstart/tsconfig.json→ extends@modelcontextprotocol/tsconfig, notarget/liboverride → effectiveesnext/[esnext]
This is a new drift introduced by the PR. The PR already fixed the module/moduleResolution drift in the same doc blocks (raised in an earlier review comment), but the consolidation onto the shared base introduced fresh drift on target/lib that wasn't propagated.
Why existing checks don't catch it
The tsconfig.json blocks in both docs are plain ```json fences with no source= attribute, so `pnpm sync:snippets --check` does not compare them against the example files. The `.ts` code snippets in the docs are `source=`-tagged and synced from the example sources — which means CI typechecks those snippets under the example's effective `target: esnext`, while the doc tells the user to compile with `ES2023`/`ES2022`.
Step-by-step proof
- Open
common/tsconfig/tsconfig.json— lines 3-4 set"target": "esnext","lib": ["esnext"]. - Open
examples/client-quickstart/tsconfig.jsonafter this PR — it has"extends": "@modelcontextprotocol/tsconfig"and itscompilerOptionsoverride setsoutDir,rootDir,declaration,declarationMap,types,pathsbut nottargetorlib. Effective target is thereforeesnext. - Open
docs/client-quickstart.md:91-92— it still shows"target": "ES2023","lib": ["ES2023"]. - Same chain for the server quickstart:
examples/server-quickstart/tsconfig.jsoninheritsesnext, butdocs/server-quickstart.md:106shows"target": "ES2022"with nolib. - Before the PR (
git show HEAD~3:examples/client-quickstart/tsconfig.json), the example was standalone with"target": "ES2023","lib": ["ES2023"]— identical to the doc block. The drift is introduced here.
Impact
No runtime breakage today — the quickstart code uses no post-ES2022/ES2023 syntax. The latent risk is that CI now typechecks the doc-synced .ts snippets under target: esnext, so a future edit using post-ES2023 syntax (e.g., a new ECMAScript proposal) would compile cleanly in CI but fail for a user who copies the doc tsconfig.json verbatim. It's primarily a documentation-consistency issue, the same class as the module/moduleResolution drift already addressed in this PR.
How to fix
Either:
- Update the doc
tsconfig.jsonblocks to"target": "esnext"/"lib": ["esnext"]so they match the example's effective config, or - Add
"target": "ES2023"/"lib": ["ES2023"](and"target": "ES2022") overrides back into the examplecompilerOptionsso the example matches what the docs advertise.
Optionally, give the doc tsconfig blocks a source= attribute (or similar) so sync:snippets can catch this class of drift going forward.
| import { Client } from '../src/client/index'; | ||
| import { SSEClientTransport } from '../src/client/sse'; | ||
| import { StdioClientTransport } from '../src/client/stdio'; | ||
| import { Server } from '../src/server/index'; | ||
| import { SSEServerTransport } from '../src/server/sse'; | ||
| import { StdioServerTransport } from '../src/server/stdio'; | ||
| import { ListResourcesResultSchema } from '../src/types'; |
There was a problem hiding this comment.
🟣 Pre-existing dead code: every relative import in scripts/cli.ts (lines 2–8) points at ../src/* paths that no longer exist post-monorepo (source now lives under packages/{client,server,core}/src/), and the file is not included in any tsconfig nor reachable from the npm scripts that reference it. Rather than continuing to mass-edit an unbuildable file via codemod, consider deleting scripts/cli.ts and the dangling server/client npm scripts in packages/{client,core,server}/package.json that point at it.
Extended reasoning...
What the bug is
The codemod that strips .js extensions touched scripts/cli.ts (lines 2–8), but every relative import in that file points at paths that ceased to exist when the repo was restructured into a monorepo:
import { Client } from '../src/client/index';
import { SSEClientTransport } from '../src/client/sse';
import { StdioClientTransport } from '../src/client/stdio';
import { Server } from '../src/server/index';
import { SSEServerTransport } from '../src/server/sse';
import { StdioServerTransport } from '../src/server/stdio';
import { ListResourcesResultSchema } from '../src/types';There is no <repo>/src/ directory. Source now lives under packages/{client,server,core}/src/. Several of the imported symbols (SSEServerTransport) don't exist anywhere in packages/ either — the server-side legacy SSE transport was removed.
Why nothing catches it
- No tsconfig sees the file. There is no root
tsconfig.json, and no package tsconfig hasscripts/in itsinclude.pnpm typecheck:allwalks per-package configs and never visitsscripts/cli.ts. - The npm scripts that reference it can't run it.
packages/client/package.json:78-79,packages/core/package.json:48-49, andpackages/server/package.json:81-82all declare scripts liketsx scripts/cli.ts server/tsx scripts/cli.ts client— but those run from the package directory, which has noscripts/subdirectory. The onlyscripts/cli.tsis at the repo root.
So the file is simultaneously unbuildable, untypechecked, and unreachable.
What this PR did to it
The codemod blindly stripped .js from each of the seven imports (the only change in the diff for this file). This neither fixes nor worsens anything — '../src/client/index.js' and '../src/client/index' are both nonexistent — but it adds churn to the diff and gives the file a false appearance of being maintained.
Step-by-step verification
ls <repo>/src→No such file or directory. The base../src/of every import in the file doesn't exist.grep -r 'SSEServerTransport' packages/→ no matches. Even the symbol on line 6 was removed in the v2 rewrite.- No
<repo>/tsconfig.jsonexists; the project's tsconfigs are per-package and none listsscripts/. packages/client/scripts/,packages/core/scripts/,packages/server/scripts/→ none exist, sotsx scripts/cli.ts ...from any of those package dirs fails withCannot find module.
Impact
None at runtime — the file was already dead and stays dead. The cost is maintenance noise: future codemods will keep touching it, and a contributor reading the diff might assume scripts/cli.ts is a working CLI.
Suggested fix
Delete scripts/cli.ts and the three dangling server/client npm scripts that point at it (in packages/client/package.json, packages/core/package.json, packages/server/package.json). This is pre-existing cleanup, not a regression introduced by this PR — but since the codemod touched the file anyway, it's a natural moment to remove it instead of continuing to edit a corpse.
Summary
common/tsconfig/tsconfig.jsonfrommodule: NodeNext/moduleResolution: NodeNexttomodule: ESNext/moduleResolution: bundler.Node16overrides inexamples/client-quickstart/tsconfig.jsonandexamples/server-quickstart/tsconfig.json..jsextensions from every relative TypeScript import acrosspackages/,examples/,scripts/, andtest/.CLAUDE.mdto reflect the new import convention.Why
Under
moduleResolution: NodeNext, every relative TS import has to be written with a.jsextension (import x from './foo.js') — a long-standing footgun for new contributors and a frequent source of confusion when scripts/codemods generate imports.moduleResolution: bundlertreats the path as a module reference, matching whattsdown,vitest, and downstream consumers' bundlers already do at runtime.This is an internal-only build configuration change. Consumers are not affected: they continue to import from the built
.mjs/.d.mtsfiles declared in each package'sexportsmap, which still carry the.jsextensions Node's NodeNext resolver requires at runtime.Test plan
pnpm typecheck:allpassespnpm lint:allpasses (includingsync:snippets --check)pnpm test:allpassespnpm build:allpasses